<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>文本转语音</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
            background-color: #f4f4f4;
            color: #333;
        }
        .container {
            background-color: #fff;
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
            max-width: 800px;
            margin: auto;
        }
        h1 {
            color: #0056b3;
            text-align: center;
        }
        .form-group {
            margin-bottom: 15px;
        }
        label {
            display: block;
            margin-bottom: 5px;
            font-weight: bold;
        }
        textarea,
        select,
        input[type="range"] {
            width: calc(100% - 22px);
            padding: 10px;
            border: 1px solid #ddd;
            border-radius: 4px;
            box-sizing: border-box;
        }
        input[type="range"] {
            padding: 0;
        }
        .range-value {
            display: inline-block;
            margin-left: 10px;
            font-weight: normal;
        }
        button {
            background-color: #007bff;
            color: white;
            padding: 10px 15px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            margin-right: 10px;
        }
        button:hover {
            background-color: #0056b3;
        }
        #audioPlayer {
            width: 100%;
            margin-top: 20px;
        }

        .loading {
            display: none;
            text-align: center;
            margin-top: 20px;
        }
        .loading::after {
            content: '';
            display: inline-block;
            width: 20px;
            height: 20px;
            border: 3px solid #f3f3f3;
            border-top: 3px solid #007bff;
            border-radius: 50%;
            animation: spin 1s linear infinite;
            margin-left: 10px;
            vertical-align: middle;
        }
        @keyframes spin {
            0% { transform: rotate(0deg); }
            100% { transform: rotate(360deg); }
        }
        .error-message {
            color: red;
            margin-top: 10px;
            text-align: center;
        }
        .subtitle-container {
            margin-top: 20px;
            padding: 15px;
            border: 1px solid #ddd;
            border-radius: 4px;
            min-height: 100px;
            max-height: 250px; /* 增加高度以显示更多字幕 */
            overflow-y: auto;
            text-align: left;
            font-size: 1.1em;
            line-height: 1.6;
            background-color: #f9f9f9;
            scroll-behavior: smooth; /* 确保滚动平滑 */
            position: relative; /* 为绝对定位的子元素提供参考 */
        }
        .subtitle-line {
            padding: 6px 10px;
            margin: 5px 0;
            color: #555;
            border-radius: 4px;
            transition: all 0.3s ease; /* 平滑过渡效果 */
        }
        .subtitle-line.highlight {
            background-color: #ffeb3b; /* 更明亮的黄色 */
            color: #000;
            font-weight: bold;
            box-shadow: 0 2px 4px rgba(0,0,0,0.15); /* 增强阴影效果 */
        }
        .subtitle-line.error {
            color: #d32f2f;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>文本转语音</h1>

        <div class="form-group">
            <label for="textInput">文本:</label>
            <textarea id="textInput" rows="10" placeholder="请输入要转换的文本...">你好，这是一个使用Edge TTS实现的文本转语音示例。</textarea>
        </div>

        <div class="form-group">
            <label for="voiceSelect">选择声音:</label>
            <select id="voiceSelect">
                <!-- 声音选项将通过 JavaScript 动态加载 -->
            </select>
        </div>

        <div class="form-group">
            <label for="rateInput">语速: <span id="rateValue">0%</span></label>
            <input type="range" id="rateInput" min="-100" max="100" value="0">
        </div>

        <div class="form-group">
            <label for="pitchInput">音调: <span id="pitchValue">0Hz</span></label>
            <input type="range" id="pitchInput" min="-50" max="50" value="0">
        </div>

        <div class="form-group">
            <label for="volumeInput">音量: <span id="volumeValue">0%</span></label>
            <input type="range" id="volumeInput" min="-100" max="100" value="0">
        </div>

        <div class="form-group">
            <label for="generateSubtitles">生成字幕:</label>
            <input type="checkbox" id="generateSubtitles" checked>
        </div>

        <button id="speakButton">生成</button>

        <div class="loading" id="loadingIndicator">正在生成语音...</div>
        <div class="error-message" id="errorMessage"></div>

        <audio id="audioPlayer" controls></audio>
        <div id="subtitleContainer" class="subtitle-container"></div>
    </div>

    <script>
        const textInput = document.getElementById('textInput');
        const voiceSelect = document.getElementById('voiceSelect');
        const rateInput = document.getElementById('rateInput');
        const rateValue = document.getElementById('rateValue');
        const pitchInput = document.getElementById('pitchInput');
        const pitchValue = document.getElementById('pitchValue');
        const volumeInput = document.getElementById('volumeInput');
        const volumeValue = document.getElementById('volumeValue');
        const speakButton = document.getElementById('speakButton');

        const audioPlayer = document.getElementById('audioPlayer');
        const generateSubtitlesCheckbox = document.getElementById('generateSubtitles');
        const subtitleContainer = document.getElementById('subtitleContainer');

        const ctx = 'http://127.0.0.1:5000';
        function loadVoices() {
            const voices = [
                { name: 'zh-CN-XiaoxiaoNeural', display: 'zh-CN-晓晓 (女) - 新闻, 小说, 温和' },
                { name: 'zh-CN-XiaoyiNeural', display: 'zh-CN-晓一 (女) - 卡通, 小说, 活泼' },
                { name: 'zh-CN-YunjianNeural', display: 'zh-CN-云健 (男) - 体育, 小说, 热情' },
                { name: 'zh-CN-YunxiNeural', display: 'zh-CN-云希 (男) - 小说, 活泼, 阳光' },
                { name: 'zh-CN-YunxiaNeural', display: 'zh-CN-云夏 (男) - 卡通, 小说, 可爱' },
                { name: 'zh-CN-YunyangNeural', display: 'zh-CN-云扬 (男) - 新闻, 专业, 可靠' },
                { name: 'zh-CN-liaoning-XiaobeiNeural', display: 'zh-CN-辽宁-小北 (女) - 方言, 幽默' },
                { name: 'zh-CN-shaanxi-XiaoniNeural', display: 'zh-CN-陕西-小妮 (女) - 方言, 开朗' },
                { name: 'zh-HK-HiuGaaiNeural', display: 'zh-HK-晓佳 (女) - 通用, 友好, 积极' },
                { name: 'zh-HK-HiuMaanNeural', display: 'zh-HK-晓曼 (女) - 通用, 友好, 积极' },
                { name: 'zh-HK-WanLungNeural', display: 'zh-HK-文龙 (男) - 通用, 友好, 积极' },
                { name: 'zh-TW-HsiaoChenNeural', display: 'zh-TW-晓辰 (女) - 通用, 友好, 积极' },
                { name: 'zh-TW-HsiaoYuNeural', display: 'zh-TW-晓羽 (女) - 通用, 友好, 积极' },
                { name: 'zh-TW-YunJheNeural', display: 'zh-TW-云哲 (男) - 通用, 友好, 积极' },
                { name: 'zu-ZA-ThandoNeural', display: 'zu-ZA-坦多 (女) - 通用, 友好, 积极' },
                { name: 'zu-ZA-ThembaNeural', display: 'zu-ZA-坦巴 (男) - 通用, 友好, 积极' }
            ];

            voiceSelect.innerHTML = '';
            voices.forEach(voice => {
                const option = document.createElement('option');
                option.value = voice.name;
                option.textContent = voice.display;
                voiceSelect.appendChild(option);
            });
        }

        function updateRateValue() {
            const rate = parseInt(rateInput.value);
            rateValue.textContent = `${rate > 0 ? '+' : ''}${rate}%`;
        }

        function updatePitchValue() {
            const pitch = parseInt(pitchInput.value);
            pitchValue.textContent = `${pitch > 0 ? '+' : ''}${pitch}Hz`;
        }

        function updateVolumeValue() {
            const volume = parseInt(volumeInput.value);
            volumeValue.textContent = `${volume > 0 ? '+' : ''}${volume}%`;
        }

        let subtitles = [];

        function parseVTT(vttContent) {
            subtitles = [];
            const lines = vttContent.split('\n');
            let i = 0;
            
            // 跳过WEBVTT头部
            while (i < lines.length && (lines[i].trim() === '' || lines[i].includes('WEBVTT'))) {
                i++;
            }
            
            while (i < lines.length) {
                // 跳过空行和数字标识符
                while (i < lines.length && (lines[i].trim() === '' || (!isNaN(parseInt(lines[i])) && isFinite(lines[i])))) {
                    i++;
                }
                
                // 检查是否是时间行
                if (i < lines.length && lines[i].includes('-->')) {
                    const timeMatch = lines[i].match(/(\d{2}:\d{2}:\d{2}[,. ]\d{3}) --> (\d{2}:\d{2}:\d{2}[,. ]\d{3})/);
                    if (timeMatch) {
                        const start = timeToSeconds(timeMatch[1]);
                        const end = timeToSeconds(timeMatch[2]);
                        i++; // 移动到文本行
                        
                        let text = '';
                        // 收集所有文本行，直到遇到空行
                        while (i < lines.length && lines[i].trim() !== '') {
                            text += (text ? ' ' : '') + lines[i].trim();
                            i++;
                        }
                        
                        if (text) {
                            subtitles.push({ start, end, text: text.trim() });
                        }
                    } else {
                        // 时间行格式不正确，跳过
                        i++;
                    }
                } else {
                    // 不是时间行，跳过
                    i++;
                }
            }
            
            console.log('解析后的字幕:', subtitles);
            return subtitles;
        }

        function timeToSeconds(timeString) {
            const parts = timeString.split(':');
            let seconds = 0;
            if (parts.length === 3) {
                // 处理秒和毫秒部分，确保正确解析小数点或逗号
                const secondsAndMs = parts[2].replace(',', '.').split('.');
                seconds = parseInt(parts[0]) * 3600 + parseInt(parts[1]) * 60 + parseInt(secondsAndMs[0]);
                if (secondsAndMs.length > 1) {
                    seconds += parseFloat('0.' + secondsAndMs[1]);
                }
            } else if (parts.length === 2) {
                seconds = parseInt(parts[0]) * 60 + parseFloat(parts[1]);
            }
            return seconds;
        }

        function displaySubtitles(vttContent) {
            const parsedSubtitles = parseVTT(vttContent);
            subtitleContainer.innerHTML = '';
            
            if (parsedSubtitles.length === 0) {
                console.warn('没有解析到有效的字幕');
                const p = document.createElement('p');
                p.classList.add('subtitle-line');
                p.textContent = '没有可用的字幕';
                subtitleContainer.appendChild(p);
                return;
            }
            
            parsedSubtitles.forEach((cue, index) => {
                const p = document.createElement('p');
                p.classList.add('subtitle-line');
                p.textContent = cue.text;
                p.dataset.index = index;
                p.dataset.start = cue.start;
                p.dataset.end = cue.end;
                subtitleContainer.appendChild(p);
            });
            
            // 重置当前字幕索引
            currentSubtitleIndex = -1;
            console.log(`已加载 ${parsedSubtitles.length} 条字幕`);
        }

        let currentSubtitleIndex = -1;
        
        audioPlayer.addEventListener('timeupdate', () => {
            const currentTime = audioPlayer.currentTime;
            let newIndex = -1;
            
            // 查找当前时间对应的字幕索引
            for (let i = 0; i < subtitles.length; i++) {
                if (currentTime >= subtitles[i].start && currentTime <= subtitles[i].end) {
                    newIndex = i;
                    break;
                }
            }
            
            // 只有当索引变化时才更新高亮和滚动
            if (newIndex !== currentSubtitleIndex) {
                // 移除之前的高亮
                if (currentSubtitleIndex !== -1) {
                    const prevLine = subtitleContainer.querySelector(`[data-index="${currentSubtitleIndex}"]`);
                    if (prevLine) {
                        prevLine.classList.remove('highlight');
                    }
                }
                
                // 添加新的高亮并滚动
                if (newIndex !== -1) {
                    const currentLine = subtitleContainer.querySelector(`[data-index="${newIndex}"]`);
                    if (currentLine) {
                        currentLine.classList.add('highlight');
                        
                        // 计算滚动位置，使高亮行居中显示
                        const containerHeight = subtitleContainer.clientHeight;
                        const lineHeight = currentLine.offsetHeight;
                        const scrollY = currentLine.offsetTop - subtitleContainer.offsetTop;
                        
                        // 检查是否需要滚动（如果当前行不在可视区域内）
                        if (scrollY < subtitleContainer.scrollTop || 
                            scrollY + lineHeight > subtitleContainer.scrollTop + containerHeight) {
                            // 计算滚动位置，使高亮行居中显示
                            subtitleContainer.scrollTop = scrollY - (containerHeight / 2) + (lineHeight / 2);
                        }
                        
                        console.log(`高亮字幕 ${newIndex}: 当前时间=${currentTime.toFixed(2)}s, 开始=${subtitles[newIndex].start.toFixed(2)}s, 结束=${subtitles[newIndex].end.toFixed(2)}s`);
                    }
                }
                
                currentSubtitleIndex = newIndex;
            }
        });

        audioPlayer.addEventListener('ended', () => {
            // 移除所有高亮
            document.querySelectorAll('.subtitle-line.highlight').forEach(line => {
                line.classList.remove('highlight');
            });
            
            // 重置字幕状态
            currentSubtitleIndex = -1;
            subtitleContainer.scrollTop = 0; // 滚动到顶部
            console.log('音频播放结束，重置字幕状态');
        });

        async function fetchAudio(action) {
            errorMessage.textContent = '';
            loadingIndicator.style.display = 'block';
            speakButton.disabled = true;
            subtitleContainer.innerHTML = ''; // 清空之前的字幕

            const selectedVoice = voiceSelect.value;
            const rate = `${parseInt(rateInput.value) >= 0 ? '+' : ''}${rateInput.value}%`;
            const pitch = `${parseInt(pitchInput.value) >= 0 ? '+' : ''}${pitchInput.value}Hz`;
            const volume = `${parseInt(volumeInput.value) >= 0 ? '+' : ''}${volumeInput.value}%`;
            const generateSubtitles = generateSubtitlesCheckbox.checked;

            if (!textInput.value.trim()) {
                errorMessage.textContent = '请输入要转换的文本。';
                loadingIndicator.style.display = 'none';
                speakButton.disabled = false;
                return;
            }

            try {
                console.log('发送TTS请求:', {
                    text: textInput.value.substring(0, 50) + (textInput.value.length > 50 ? '...' : ''),
                    voice: selectedVoice,
                    rate,
                    pitch,
                    volume,
                    generate_subtitles: generateSubtitles
                });
                
                const response = await fetch(ctx + '/tts', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({
                        text: textInput.value,
                        voice: selectedVoice,
                        rate: rate,
                        pitch: pitch,
                        volume: volume,
                        generate_subtitles: generateSubtitles
                    }),
                });

                if (!response.ok) {
                    const errorText = await response.text();
                    try {
                        const errorData = JSON.parse(errorText);
                        throw new Error(errorData.error || '未知错误');
                    } catch (e) {
                        throw new Error(`服务器错误 (${response.status}): ${errorText || '未知错误'}`);
                    }
                }

                const responseData = await response.json();
                console.log('收到TTS响应:', responseData);
                
                if (responseData.audio_url) {
                    const audioUrl = ctx + responseData.audio_url;
                    console.log('音频URL:', audioUrl);
                    audioPlayer.src = audioUrl;
                    
                    if (action === 'play') {
                        audioPlayer.play();
                    }
                    
                    // 处理字幕
                    if (responseData.vtt && generateSubtitles) {
                        console.log('收到VTT数据，长度:', responseData.vtt.length);
                        displaySubtitles(responseData.vtt);
                    } else if (generateSubtitles) {
                        console.warn('未收到字幕数据');
                        subtitleContainer.innerHTML = '<p class="subtitle-line">未能生成字幕</p>';
                    }
                } else {
                    throw new Error('响应中没有音频URL');
                }

            } catch (error) {
                errorMessage.textContent = '生成语音失败: ' + error.message;
                console.error('获取音频时出错:', error);
                subtitleContainer.innerHTML = '<p class="subtitle-line error">生成失败</p>';
            } finally {
                loadingIndicator.style.display = 'none';
                speakButton.disabled = false;
            }
        }

        // Initial loads and event listeners
        loadVoices();
        updateRateValue();
        updatePitchValue();
        updateVolumeValue();

        rateInput.addEventListener('input', updateRateValue);
        pitchInput.addEventListener('input', updatePitchValue);
        volumeInput.addEventListener('input', updateVolumeValue);
        speakButton.addEventListener('click', () => fetchAudio('play'));

        // Ensure highlightedTextDiv is visible when content is present
        // This is handled within displayHighlightedText now, but keeping this for clarity
        // if (highlightedTextDiv.innerHTML.trim() !== '') {
        //     highlightedTextDiv.style.display = 'block';
        // }

    </script>
</body>
</html>